package com.bupt.uchouten.common.entity;

import com.bupt.uchouten.common.register.UchoutenEntities;
import com.bupt.uchouten.common.skill.Earthquake;
import com.bupt.uchouten.common.util.PixelmonUtil;
import com.bupt.uchouten.common.util.UchoutenLogger;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket;
import net.minecraft.network.protocol.game.ClientboundSetEntityMotionPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.OwnableEntity;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.entity.IEntityAdditionalSpawnData;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.fml.ModList;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

public class EarthquakeEntity extends Entity implements OwnableEntity, IEntityAdditionalSpawnData {

    private UUID casterUUID;

    /**
     * The time in ticks this magical generator lasts for; defaults to 600 (30 seconds). If this is -1 the generator
     * doesn't despawn.
     */
    public int lifetime = 600;

    /**
     * The damage multiplier for this generator, determined by the wand with which it was cast.
     */
    public float damageMultiplier = 1.0f;

    public EarthquakeEntity(Level level) {
        super(UchoutenEntities.EARTHQUAKE_ENTITY, level);
    }

    private boolean canDamageBlocks(Entity entity, Level world) {
        return Optional.ofNullable(entity)
                .map(e -> e instanceof Player ? ((Player) e).mayBuild() : ForgeEventFactory.getMobGriefingEvent(world, e))
                .orElse(true);
    }

    private boolean isBlockUnbreakable(BlockState blockState) {
        return !blockState.isAir() && blockState.getBlock().defaultDestroyTime() == -1.0f;
    }

    public static <T extends Entity> List<T> getEntitiesWithinRadius(double radius, double x, double y, double z, Level world, Class<T> entityType) {
        AABB aabb = new AABB(x - radius, y - radius, z - radius, x + radius, y + radius, z + radius);
        List<T> entityList = world.getEntitiesOfClass(entityType, aabb);
        for (int i = 0; i < entityList.size(); i++) {
            // distanceToSqr() doesn't do sqrt, only sqr
            if (entityList.get(i).distanceToSqr(x, y, z) > radius * radius) {
                entityList.remove(i);
                break;
            }
        }
        return entityList;
    }

    @Override
    public void tick() {
        // generator tick
        if (this.tickCount > lifetime && lifetime != -1) {
            this.despawn();
        }

        super.tick();

        // This tick
        double speed = Earthquake.SPREAD_SPEED;
        Level world = this.level();

        if (!world.isClientSide() && canDamageBlocks(this.getCaster(), world)) {
            // The further the earthquake is going to spread, the finer the angle increments.
            for (float angle = 0; angle < 2 * Math.PI; angle += (float) (Math.PI / (lifetime * 1.5))) {

                // Calculates coordinates for the block to be moved. The radius increases with time. The +1.5 is to
                // leave blocks in the centre untouched.
                int x = this.getX() < 0 ? (int) (this.getX() + ((this.tickCount * speed) + 1.5) * Math.sin(angle) - 1)
                        : (int) (this.getX() + ((this.tickCount * speed) + 1.5) * Math.sin(angle));
                int y = (int) Math.floor(this.getY() - 0.5);
                int z = this.getZ() < 0 ? (int) (this.getZ() + ((this.tickCount * speed) + 1.5) * Math.cos(angle) - 1)
                        : (int) (this.getZ() + ((this.tickCount * speed) + 1.5) * Math.cos(angle));

                BlockPos pos = new BlockPos(x, y, z);
                BlockState blockState = world.getBlockState(pos);
                BlockState aboveBlockState = world.getBlockState(pos.above());

                // Checks that the block above is not solid, since this causes the falling sand to vanish.
                // Original: Use blockState.isCollisionShapeFullBlock(world, pos) && !world.getBlockState(pos.above()).isCollisionShapeFullBlock(world, pos.above())
                // New:
                if (!isBlockUnbreakable(blockState) && !blockState.hasBlockEntity()
                        && !aboveBlockState.isCollisionShapeFullBlock(world, pos.above()) && !aboveBlockState.hasBlockEntity()
                        && blockState.canEntityDestroy(world, pos, getCaster())) {
                    // Falling blocks do the setting block to air themselves.
                    // Refer FallingBlockEntity#fall() to update
                    // Currently don't use FallingBlockEntity#fall() since it just falls, don't flip up
//                    FallingBlockEntity fallingblock = FallingBlockEntity.fall(world, pos, blockState);
                    FallingBlockEntity fallingBlockEntity = new FallingBlockEntity(EntityType.FALLING_BLOCK, world);
                    fallingBlockEntity.blocksBuilding = true;
                    fallingBlockEntity.setPos(x + 0.5D, y + 0.5D, z + 0.5D);
                    fallingBlockEntity.lerpMotion(0, 0.3, 0);
                    fallingBlockEntity.xo = x;
                    fallingBlockEntity.yo = y;
                    fallingBlockEntity.zo = z;
                    fallingBlockEntity.setStartPos(fallingBlockEntity.blockPosition());
                    ClientboundAddEntityPacket packet = new ClientboundAddEntityPacket(fallingBlockEntity, Block.getId(blockState));
                    fallingBlockEntity.recreateFromPacket(packet);
                    world.setBlock(pos, blockState.getFluidState().createLegacyBlock(), 3);
                    world.addFreshEntity(fallingBlockEntity);
                }
            }

        }

        List<LivingEntity> targets = getEntitiesWithinRadius((this.tickCount * speed) + 1.5, this.getX(), this.getY(), this.getZ(), world, LivingEntity.class);
        // In this particular instance, the caster is completely unaffected because they will always be in the
        // centre.
        targets.remove(this.getCaster());

        for (LivingEntity target : targets) {
            // Searches in a 1 wide ring.
            if (this.distanceTo(target) > (this.tickCount * speed) + 0.5 && target.getY() < this.getY() + 1
                    && target.getY() > this.getY() - 1) {

                if (this.isValidTarget(this.getCaster(), target)) {
                    target.hurt(world.damageSources().inWall(), 10 * this.damageMultiplier);
                    target.addEffect(new MobEffectInstance(MobEffects.WEAKNESS, 400, 1));
                }

                // Knockback must be removed in this instance, or the target will fall into the floor.
                // All targets are thrown, even those immune to the damage, so they don't fall into the ground.
                // Throws target into the air.
                target.lerpMotion(0, 0.8, 0);

                // Player motion is handled on that player's client so needs packets
                if (target instanceof ServerPlayer) {
                    ((ServerPlayer) target).connection.send(new ClientboundSetEntityMotionPacket(target));
                }
            }
        }
    }

    public void despawn() {
        this.remove(RemovalReason.DISCARDED);
    }

    /**
     * With the owner of this generator as the
     * attacker. Also allows subclasses to override it if they wish to do so.
     */
    public boolean isValidTarget(Entity attacker, Entity target) {
//        // Owned entities inherit their owner's allies
//        if(attacker instanceof OwnableEntity && !isValidTarget(((OwnableEntity)attacker).getOwner(), target)) {
//            return false;
//        }
//
//        // Tests whether the target is a creature that was summoned/tamed (or is otherwise owned) by the attacker
//        if(target instanceof OwnableEntity &&
//                attacker instanceof LivingEntity &&
//                !(((LivingEntity)attacker).getRevengeTarget() == ((OwnableEntity)target).getOwner() ||
//                        ((LivingEntity)attacker).getAttackTarget() == ((OwnableEntity)target).getOwner())){
//            return false;
//        }
        // Separately judge pokemons
        if (ModList.get().isLoaded("pixelmon") && PixelmonUtil.checkIsPixelmon(target)) {
            return false;
        }

        // Always return false if the target is null
        // Always return true if the attacker is null - this must be after the target null check!
        return attacker == null || Optional.ofNullable(target)
                // Tests whether the target is the attacker
                .filter(t -> t != attacker)
                // Tests whether the target is a creature that was summoned/tamed (or is otherwise owned) by the attacker
                .map(t -> !(t instanceof OwnableEntity && ((OwnableEntity) t).getOwner() == attacker))
                .orElse(false);
    }

    public void setCaster(@Nullable LivingEntity caster) {
        Optional.ofNullable(caster).ifPresent(casterNonNull -> this.casterUUID = casterNonNull.getUUID());
    }

    /**
     * Returns the EntityLivingBase that created this generator, or null if it no longer exists. Cases where the entity
     * may no longer exist are: entity died or was deleted, mob despawned, player logged out, entity teleported to
     * another dimension, or this generator simply had no caster in the first place.
     */
    @Nullable
    public LivingEntity getCaster() { // Kept despite the above method because it returns an EntityLivingBase
        return this.getOwner();
    }

    @Override
    protected void defineSynchedData() {
    }

    @Override
    protected void readAdditionalSaveData(CompoundTag compoundTag) {
        if (compoundTag.hasUUID("casterUUID")) {
            casterUUID = compoundTag.getUUID("casterUUID");
        }
        this.lifetime = compoundTag.getInt("lifetime");
        this.damageMultiplier = compoundTag.getFloat("damageMultiplier");
    }

    @Override
    protected void addAdditionalSaveData(CompoundTag compoundTag) {
        compoundTag.putUUID("casterUUID", casterUUID);
        compoundTag.putInt("lifetime", lifetime);
        compoundTag.putFloat("damageMultiplier", damageMultiplier);
    }

    @Nullable
    @Override
    public UUID getOwnerUUID() {
        return casterUUID;
    }

    @Override
    public void writeSpawnData(FriendlyByteBuf buffer) {
        buffer.writeInt(lifetime);
        buffer.writeInt(getCaster() == null ? -1 : getCaster().getId());
    }

    @Override
    public void readSpawnData(FriendlyByteBuf additionalData) {
        lifetime = additionalData.readInt();

        int id = additionalData.readInt();

        if (id == -1) {
            setCaster(null);
        } else {
            Entity entity = this.level().getEntity(id);
            if (entity instanceof LivingEntity) {
                setCaster((LivingEntity) entity);
            } else {
                UchoutenLogger.warn("Generator caster with ID %d in spawn data not found", id);
            }
        }
    }
}
